[자유 주제] R을 이용한 서울시 아파트 실거래가 회귀분석 및 주택가격 결정 요인

이종호

1 개요

  • 2016년 2월 1일부터 국토교통부를 통해 수집된 아파트매매 실거래 자료를 공공데이터포털 홈페이지 (https://www.data.go.kr/data/15058747/openapi.do)에 공개하고 있어 부동산 거래 가격 및 거래 동향을 정확하고 빠르게 파악할 수 있음∙

  • 특히, 전국 주소기반 부동산 거래 자료가 온라인상에서 투명하게 공개되고 있어 부동산 시장에 대한 흐름을 한눈에 파악할 수 있는 충분한 양의 공간데이터가 구축된 상황 (아파트의 경우 매년 약 50만 개의 데이터가 등록되고 있으며 자세한 주소 정보와 상세 거래월 등을 포함하고 있기 때문에 부동산 흐름에 대한 시기별 변화를 공간상에 표출할 수 있어 부동산에 대한 정보를 국민들에게 보다 쉽게 전달할 수 있게 되었음)

  • 따라서 데이터로의 접근이 용이하고 부동산 시장에 대한 흐름을 파악할 수 있는 좋은 데이터가 구축되었음에 따라 실거래 가격 자료를 활용하여 2017년부터 2021년까지 (간격: 매월)의 부동산 시장의 변화를 정책적 함의를 배제한 시간과 공간만의 접근 방식으로 부동산 시장의 변화를 파악해 보고자 함

  • 특히 데이터 통계값을 활용하여 차트나 그래프로 세밀한 부동산 시장 흐름을 파악

  • 더 나아가 데이터의 고차원화를 통해 부동산 시장의 공간과 시간상의 변화를 한눈에 볼 수 있는 지도 시각화 수행

2 주요 내용

  • 약 364,917개의 데이터 처리를 위해 빅데이터 처리 연산프로그램인 R 프로그래밍 (+ Pyhon 동일)을 활용

  • 아파트 실거래 가격 데이터를 이용하여 연소득당 거래금액 (주택 가격 결정 요인) 계산 및 기초통계 분석

  • 특히 전체 및 법정동을 구분하여 히스토그램, 상자, 산점도 그래프뿐만 아니라 공간 분포 시각화 수행

  • 또한 주택 가격 결정 요인을 분석하기 위해서 다음과 같은 독립변수 및 종속변수를 설정하여 회귀분석을 수행

  • 독립변수 : 건축년도, 전용면적, 층, 법정동, 면적당 거래금액

  • 종속변수 : 연소득당 거래금액 (주택 가격 결정 요인)

3 자료 정제

3.1 R 프로그래밍을 위한 환경 변수 설정

3.2 필요한 라이브러리 및 변수명 읽기

3.3 서울특별시 법정동 코드 읽기

  • 자료 설명 : 법정동 코드 목록
  • 수집 방법 : 해당 URL에서 자료 다운로드
  • 자료 개수 : 46,180개
  • URL : https://www.code.go.kr/stdcode/regCodeL.do
  • 출처 : 행정표준코드관리시스템
codeInfo = Sys.glob(paste(globalVar$mapPath, "/admCode/법정동코드_전체자료.txt", sep = "/"))

codeList = readr::read_delim(codeInfo, delim = "\t", locale = locale("ko", encoding = "EUC-KR"), col_types = "ccc") %>%
  magrittr::set_colnames(c("EMD_CD", "addr", "isUse")) %>% 
  tidyr::separate(col = "addr", into = c("d1", "d2", "d3", "d4"), sep = " ") %>%
  dplyr::mutate(
    emdCd = stringr::str_sub(EMD_CD, 1, 5)
  ) %>% 
  dplyr::filter(
    stringr::str_detect(d1, regex("서울특별시"))
    , stringr::str_detect(isUse, regex("존재"))
    , is.na(d3)
    , is.na(d4)
  )

codeDistList = codeList %>%
  dplyr::distinct(emdCd)

# 날짜 기간
# dtDateList = seq(as.Date("2017-01-01"), as.Date(format(Sys.time(), "%Y-%m-%d")), "1 month")
dtDateList = seq(as.Date("2018-12-01"), as.Date(format(Sys.time(), "%Y-%m-%d")), "1 month")



3.4 법정동 소득 자료 읽기

fileInfo = Sys.glob(paste(globalVar$inpPath, "LSH0178_가구_특성정보_(+소득정보)_201211.csv", sep = "/"))
costData = readr::read_csv(file = fileInfo) %>%
  dplyr::mutate(
    emdCd = stringr::str_sub(as.character(raw_dn_cd), 1, 5)
  ) %>% 
  dplyr::group_by(emdCd) %>% 
  dplyr::summarise(
    meanCost = mean(avrg_income_amount_am, na.rm = TRUE)
  )



3.5 아파트 실거래 데이터 수집 및 읽기

  • 자료 설명 : 국토교통부_아파트매매 실거래자료
  • 수집 방법 : 공공데이터포털에서 오픈 API 자료 수집
  • 자료 기간 : 2017년 – 2020년 05월
  • 자료 개수 : 364,917개
  • URL : https://www.data.go.kr/data/15058747/openapi.do
  • 출처 : 공공데이터포털 (국토교통부)
#***********************************************
# 공공데이터포털 API (자료 수집)
#***********************************************
# dataL1 = tibble::tibble()
# 
# for (i in 1:length(dtDateList)) {
#   for (j in 1:nrow(codeDistList)) {
#     
#     sDate = format(dtDateList[i], "%Y%m")
#     
#     # 요청 법정동
#     reqLawdCd = stringr::str_c("&LAWD_CD=", codeDistList[j, 'emdCd'])
#     
#     # 요청 날짜
#     reqYmd = stringr::str_c("&DEAL_YMD=", sDate)
#     
#     resData = httr::GET(
#       stringr::str_c(reqUrl, reqKey, reqLawdCd, reqYmd)
#     ) %>%
#       httr::content(as = "text", encoding = "UTF-8") %>%
#       jsonlite::fromJSON()
#     
#     resCode = resData$response$header$resultCode
#     if (resCode != "00") { next }
#     
#     resItems = resData$response$body$items
#     if (resItems == "") { next }
#     
#     cat(sprintf(
#       "dtDate : %10s | code : %5s"
#       , sDate
#       , codeList[j, 'emdCd']
#     ), "\n")
#     
#     resItem = resItems$item %>%
#       as.data.frame()
#     # readr::type_convert()
#     
#     dataL1 = dplyr::bind_rows(
#       dataL1
#       , data.frame(
#         'dtYm' = sDate
#         , 'emdCd' = codeDistList[j, 'emdCd']
#         , resItem
#       )
#     )
#   }
# }

#***********************************************
# 자료 저장
#***********************************************
# saveFile = sprintf("%s/%s_%s", globalVar$outPath, serviceName, "seoul apartment transaction.csv")
# readr::write_csv(x = dataL1, file = saveFile)



4 데이터 전처리

fileInfo = Sys.glob(paste(globalVar$inpPath, "LSH0178_seoul apartment transaction.csv", sep = "/"))

dataL2 = readr::read_csv(file = fileInfo) %>% 
  readr::type_convert() %>% 
  dplyr::mutate(
    지번2 = readr::parse_number(지번)
    , emdCd = as.character(emdCd)
  ) %>% 
  dplyr::left_join(codeList, by = c("emdCd" = "emdCd")) %>%
  dplyr::left_join(costData, by = c("emdCd" = "emdCd")) %>% 
  dplyr::mutate(
    addr = stringr::str_trim(paste(d1, d2, 아파트, 지번, seq = ' '))
    , val = 거래금액 / meanCost # 연소득당 거래금액
    , val2 = 거래금액 / 전용면적 # 면적당 거래금액
    , dtYear = lubridate::year(lubridate::ym(dtYm))
  )


dataL3 = dataL2 %>% 
  dplyr::group_by(d2) %>% 
  dplyr::summarise(
    meanVal = mean(val, na.rm = TRUE)
  )



5 데이터 요약 (기초 통계량, 표/그래프 활용)

5.1 요약 통계량

5.1.1 연소득당 거래 금액에 대해서 평균값, 중앙값, 표준편차, 최대값, 최소값 계산

# 연소득당 거래금액 따른 기초 통계량
dataL2 %>%
  dplyr::summarise(
    meanVal = mean(val, na.rm = TRUE) # 평균값
    , medianVal = median(val, na.rm = TRUE) # 중앙값
    , sdVal = sd(val, na.rm = TRUE) # 표준편차
    , maxVal = max(val, na.rm = TRUE) # 최대값
    , minVal = min(val, na.rm = TRUE) # 최소값
    , cnt = n() # 개수
  ) %>%
  dplyr::arrange(desc(meanVal))
## # A tibble: 1 x 6
##   meanVal medianVal sdVal maxVal minVal    cnt
##     <dbl>     <dbl> <dbl>  <dbl>  <dbl>  <int>
## 1    14.1      12.6  7.87   150.   1.25 364917
# 법정동에 따른 연소득당 거래금액 따른 기초 통계량
dataL2 %>%
  dplyr::group_by(d2) %>% 
  dplyr::summarise(
    meanVal = mean(val, na.rm = TRUE) # 평균값
    , medianVal = median(val, na.rm = TRUE) # 중앙값
    , sdVal = sd(val, na.rm = TRUE) # 표준편차
    , maxVal = max(val, na.rm = TRUE) # 최대값
    , minVal = min(val, na.rm = TRUE) # 최소값
    , cnt = n() # 개수
  ) %>%
  dplyr::arrange(desc(meanVal))
## # A tibble: 25 x 7
##    d2       meanVal medianVal sdVal maxVal minVal   cnt
##    <chr>      <dbl>     <dbl> <dbl>  <dbl>  <dbl> <int>
##  1 용산구      24.3      19.6 18.2   150.    2.23  7665
##  2 강남구      20.0      18.3 10.6   146.    1.90 20425
##  3 서초구      18.4      16.8  9.65   66.9   1.64 15881
##  4 송파구      17.3      15.3  8.34   68.5   2.25 24080
##  5 성동구      17.2      15.6  8.66  127.    2.28 14320
##  6 광진구      17.1      15.9  7.61   58.0   1.39  7194
##  7 마포구      16.3      15.3  6.77   65.5   2.32 13400
##  8 영등포구    15.3      13.6  8.61   94.3   1.89 15163
##  9 동작구      15.0      14.2  5.67   55.2   1.90 13089
## 10 양천구      15.0      12.9  8.53   57.6   1.56 16981
## # ... with 15 more rows



5.2 표/그래프 활용

5.2.1 연소득당 거래금액 따른 히스토그램

  • 서울특별시 아파트 가격 동향을 살펴보기 위해서 2017-2020년 05월 기간 동안 연소득당 거래금액에 따른 히스트그램으로 나타냄. 그 결과 0-150으로 다양하게 형성되고 있으며 특히 중간값 (12.55)을 기준으로 평균 (14.14)로서 우측 편향됨. 이는 교환가치 측면에서 중・대형 아파트와 비교하여 60㎡ 이하의 소형아파트는 투자가치가 높아 수요가 증가함을 확인할 수 있음 (장동훈 외, 2013)
# 연소득당 거래금액 따른 히스토그램
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "연소득당 거래금액 따른 히스토그램")

ggplot(dataL2, aes(x = val)) +
  geom_histogram(aes(y = ..density..), colour = "black", fill = "white") +
  geom_density(alpha = 0.2) +
  geom_rug(aes(x = val, y = 0), position = position_jitter(height = 0)) +
  labs(x = "연소득당 거래금액", y = "밀도 함수", colour = NULL, fill = NULL, subtitle = "연소득당 거래금액 따른 히스토그램") +
  theme(text = element_text(size = 16)) # +

  # ggsave(filename = saveImg, width = 12, height = 6, dpi = 600)



5.2.2 법정동에 따른 연소득당 거래금액 히스토그램

  • 서울특별시 지역구 (법정동)에 대한 아파트 가격 동향을 살펴보기 위해서 2017-2020년 05월 기간 동안 연소득당 거래금액에 따른 히스트그램으로 나타냄. 그 결과 지역구마다 9-24로 다양하게 형성되고 있으며 서울시 전역에 걸쳐 꾸준히 거래 이루어짐 (김정희, 서울시 아파트 실거래가의 변화패턴 분석)

  • 특히 서울 중심 지역구의 경우 다른 지역보다 2-2.5배의 연소득당 거래금액을 보였고 특히 용산구는 최대값 (24)을 보였다. 반면에 서울 외곽 지역구 (금천구, 도봉구 등)에서는 낮은값 (9-10)을 보였다. 이는 용산구의 경우 2019년 9월 13일 정부 부동산 정책 이후로 저소득 계층이 사용하던 주택 (재개발 구역 노후 주택)이 재개발되면서 고소득 계층이 밀려오는 현상으로 판단됨 (장희순 강원대 부동산학과 교수)

# 법정동에 따른 연소득당 거래금액 히스토그램
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "법정동에 따른 연소득당 거래금액 히스토그램")

ggplot(dataL3, aes(x = d2, y = meanVal, fill = meanVal)) +
  geom_bar(position = "dodge", stat = "identity") +
  geom_text(aes(label = round(meanVal, 0)), vjust = 1.6, color = "white", size = 4) +
  labs(x = "법정동", y = "연소득당 거래금액", fill = NULL, subtitle = "법정동에 따른 연소득당 거래금액 히스토그램") +
  scale_fill_gradientn(colours = cbMatlab, na.value = NA) +
  theme(
    text = element_text(size = 16)
    , axis.text.x = element_text(angle = 45, hjust = 1)
  ) # +

  # ggsave(filename = saveImg, width = 12, height = 8, dpi = 600)



5.2.3 연소득당 거래금액 따른 상자 그림

  • 서울특별시에 대한 아파트의 과도한 가격 및 안정화 여부를 살펴보기 위해서 2017-2020년 05월 기간 동안 연소득당 거래금액에 따른 상자그림으로 나타냄. 그 결과 중간값 (12.55)에 대비하여 최대 150까지 분포되고 있으며 이는 연봉 1억 고소득자가 150년 동안 한푼도 안써야 살 수 있는 아파트이다. 즉 이는 정부의 잇다른 정책 실패와 전세난에 지친 무주택자들이 서울 중저가 아파트를 중심으로 매수로 나선 영향을 판단된다.
# 연소득당 거래금액 따른 상자 그림
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "연소득당 거래금액 따른 상자 그림")

ggplot(dataL2, aes(y = val)) +
  geom_boxplot() +
  labs(x = NULL, y = "연소득당 거래금액", colour = NULL, fill = NULL, subtitle = "연소득당 거래금액 따른 상자 그림") +
  theme(text = element_text(size = 16)) # +

  # ggsave(filename = saveImg, width = 12, height = 6, dpi = 600)



5.2.4 법정동에 따른 연소득당 거래금액 상자 그림

  • 서울특별시 지역구별로 상자 그림을 시각화 수행

  • 앞선 히스토그램에서와 같이 서울 중심 지역구의 경우 다른 지역보다 높은 이상치 (IQR 75% 이상)를 보였고 특히 용산구는 큰 범위 (2-150)을 보였다.

  • 반면에 서울 외곽 지역구 (노원구, 도봉구 등)에서는 낮은 거래 금액 (1.4-36)을 보였다.

# 법정동에 따른 연소득당 거래금액 상자 그림
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "법정동에 따른 연소득당 거래금액 상자 그림")

ggplot(dataL2, aes(x = d2, y = val, color = d2)) +
  geom_boxplot() +
  labs(x = "법정동", y = "연소득당 거래금액", color = "법정동", fill = NULL, subtitle = "법정동에 따른 연소득당 거래금액 상자 그림") +
  # scale_colour_gradientn(colours = cbMatlab, na.value = NA) +
  theme(
    text = element_text(size = 16)
    , axis.text.x = element_text(angle = 45, hjust = 1)
  ) # +

  # ggsave(filename = saveImg, width = 12, height = 8, dpi = 600)



5.2.5 연소득당 거래금액 산점도

  • 서울특별시에 대한 연소득과 아파트 가격 가격과의 관계성을 파악하기 위해서 2017-2020년 05월 기간 동안 지역구의 연소득 및 거래금액을 산점도로 나타냄. 그 결과 연소득이 증가할수록 거래금액도 증가 경향 (상관계수 = 0.59)이고 이는 유의수준 0.01 이하로서 통계적으로 유의미함을 보임. 이와 더불어 선형회귀곡선에서는 연봉대비 27배의 거래금액으로 파악되며 특히 95% 신뢰구간의 이상값의 경우 비교적 낮은 연봉임에도 불구하고 정부 정책의 대출 규제 완화되면서 70% 이상의 빚 보증과 더불어 영끌로 내집 마련자로 파악된다.
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "연소득당 거래금액 산점도")

ggpubr::ggscatter(
  dataL2, x = "meanCost", y = "거래금액"
  , add = "reg.line", conf.int = TRUE, scales = "free_x"
  # , facet.by = "전체 법정동"
  , add.params = list(color = "blue", fill = "lightblue")
) +
  labs(
    title = NULL
    , x = "연소득"
    , y = "거래금액"
    , color = NULL
    , subtitle = "연소득당 거래금액 산점도"
  ) +
  theme_bw() +
  ggpubr::stat_regline_equation(label.x.npc = 0.0, label.y.npc = 1.0, size = 5) +
  ggpubr::stat_cor(label.x.npc = 0.0, label.y.npc = 0.90, p.accuracy  =  0.01,  r.accuracy  =  0.01, size = 5) +
  theme(text = element_text(size = 16)) # +

  # ggsave(filename = saveImg, width = 8, height = 8, dpi = 600)



5.3 공간 분석 (ggmap, idw, tmap 활용)

5.3.1 ggmap를 활용하여 연소득당 거래금액 지도 시각화

addrList = dataL2$addr %>% unique() %>% sort() %>%
  as.tibble()

# 구글 API 하루 제한
# addrData = ggmap::mutate_geocode(addrList, value, source = "google")

# 각 주소에 따라 위/경도 반환
# for (i in 1:nrow(addrList)) {
#   addrData = ggmap::mutate_geocode(addrList[i, 'value'], value, source = "google")
# 
#   if (nrow(addrData) < 1) { next }
# 
#   readr::write_csv(x = addrData, file = saveFile, append = TRUE)
# }

saveFile = sprintf("%s/%s_%s.csv", globalVar$inpPath, serviceName, "seoul apartment transaction-addrData")
addrData =  readr::read_csv(file = saveFile, col_names = c("value", "lon", "lat"))

dataL4 = dataL2 %>% 
  dplyr::left_join(addrData, by = c("addr" = "value")) %>% 
  dplyr::filter(
    ! is.na(lon)
    , ! is.na(lat)
    , dplyr::between(lon, 120, 130)
    , dplyr::between(lat, 30, 40)
  ) %>% 
  dplyr::group_by(lon, lat, addr) %>% 
  dplyr::summarise(
    meanVal = mean(val, na.rm = TRUE)
  )


map = ggmap::get_map(
  location = c(lon = mean(dataL4$lon, na.rm = TRUE), lat = mean(dataL4$lat, na.rm = TRUE))
  , zoom = 12
  , maptype = "hybrid"
)

saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "연소득당 거래금액 지도 매핑")

ggmap(map, extent = "device") +
  geom_point(data = dataL4, aes(x = lon, y = lat, color = meanVal, size = meanVal, alpha = 0.3)) +
  scale_color_gradientn(colours = cbMatlab, na.value = NA) +
  labs(
    subtitle = NULL
    , x = NULL
    , y = NULL
    , fill = NULL
    , colour = NULL
    , title = NULL
    , size = NULL
  ) +
  scale_alpha(guide = 'none') +
  theme(
    text = element_text(size = 18)
  ) # +

  # ggsave(filename = saveImg, width = 10, height = 10, dpi = 600)



5.3.2 idw를 활용하여 연도별 연소득당 거래금액 지도 시각화

#***********************************************
# IDW 지도 그리기
#***********************************************
dtYearList = dataL2$dtYear %>% unique() %>% sort()

# dtYearInfo = 2017
for (dtYearInfo in dtYearList) {
  
  dataL5 = dataL2 %>% 
    dplyr::left_join(addrData, by = c("addr" = "value")) %>% 
    dplyr::filter(
      ! is.na(lon)
      , ! is.na(lat)
      , dplyr::between(lon, 120, 130)
      , dplyr::between(lat, 30, 40)
      , dtYear == dtYearInfo
    ) %>% 
    dplyr::group_by(lon, lat, addr) %>% 
    dplyr::summarise(
      meanVal = mean(val, na.rm = TRUE)
    )
  
  # 면적당 거래금액 지도 집중도
  spNewData = expand.grid(
    x = seq(from = min(dataL5$lon, na.rm = TRUE), to = max(dataL5$lon, na.rm = TRUE), by = 0.003)
    , y = seq(from = min(dataL5$lat, na.rm = TRUE), to = max(dataL5$lat, na.rm = TRUE), by = 0.003)
  )
  sp::coordinates(spNewData) = ~ x + y
  sp::gridded(spNewData) = TRUE
  
  spData = dataL5
  sp::coordinates(spData) = ~ lon + lat
  
  # IDW 학습 및 전처리수행
  spDataL1 = gstat::idw(
    formula = meanVal ~ 1
    , locations = spData
    , newdata = spNewData
    , nmax = 4
  ) %>%
    as.data.frame() %>%
    dplyr::rename(
      lon = x
      , lat = y
      , val = var1.pred
    ) %>%
    dplyr::select(-var1.var) %>% 
    as.tibble()
  
  summary(spDataL1)
  saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, stringr::str_c(dtYearInfo, "년 연소득당 거래금액 IDW 지도 매핑"))
  
resPlot = ggmap(map, extent = "device") +
    geom_tile(data = spDataL1, aes(x = lon, y = lat, fill = val, alpha = 0.2)) +
    # geom_raster(data = spDataL1, aes(x = lon, y = lat, fill = val, alpha = 0.2)) +
    # scale_color_gradientn(colours = cbMatlab, na.value = NA) +
    scale_fill_gradientn(colours = cbMatlab, na.value = NA) +
    labs(
      subtitle = stringr::str_c(dtYearInfo, "년 연소득당 거래금액 IDW 지도 매핑")
      , x = NULL
      , y = NULL
      , fill = NULL
      , colour = NULL
      , title = NULL
      , size = NULL
    ) +
    scale_alpha(guide = 'none') +
    theme(
      text = element_text(size = 16)
    ) # +
    # ggsave(filename = saveImg, width = 10, height = 10, dpi = 600)

    print(resPlot)
}

[inverse distance weighted interpolation] [inverse distance weighted interpolation] [inverse distance weighted interpolation] [inverse distance weighted interpolation] [inverse distance weighted interpolation]

5.3.3 tmap를 활용하여 전체/연도별 연소득당 거래금액 지도 시각화

#==========================================
# TMAP 주제도 그리기
#==========================================
mapInfo = Sys.glob(paste(globalVar$mapPath, "/admCode/TL_SCCO_SIG.shp", sep = "/"))
mapShape = sf::st_read(mapInfo, options = "ENCODING=EUC-KR")
## options:        ENCODING=EUC-KR 
## Reading layer `TL_SCCO_SIG' from data source `E:\04. TalentPlatform\Github\TalentPlatform-R\src\markDown\admCode\TL_SCCO_SIG.shp' using driver `ESRI Shapefile'
## Simple feature collection with 250 features and 3 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: 746110.3 ymin: 1458754 xmax: 1387948 ymax: 2068444
## Projected CRS: PCS_ITRF2000_TM
# 전체 법정동에 따른 연소득당 거래금액 주제도
dataL6 = dataL2 %>% 
  dplyr::left_join(addrData, by = c("addr" = "value")) %>% 
  dplyr::filter(
    ! is.na(lon)
    , ! is.na(lat)
    , dplyr::between(lon, 120, 130)
    , dplyr::between(lat, 30, 40)
  ) %>% 
  dplyr::group_by(emdCd) %>%
  dplyr::summarise(
    meanVal = mean(val, na.rm = TRUE)
  )

mapShapeL1 = mapShape %>% 
  dplyr::inner_join(dataL6, by = c("SIG_CD" = "emdCd"))

setTilte = "전체 법정동에 따른 연소득당 거래금액 주제도"
# saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName,  setTilte)

tmap::tmap_mode("view")

tmap::tm_shape(mapShapeL1) +
  tmap::tm_polygons(col = "meanVal", alpha = 0.5, palette = cbMatlab, legend.hist = TRUE, style = "cont") +
  tmap::tm_text(text = "SIG_KOR_NM") +
  tmap::tm_legend(outside = TRUE) + 
  tmap::tm_layout(title = setTilte)
  # tmap_save(filename = saveImg, width = 10, height = 10, dpi = 600)
# 연도별 법정동에 따른 연소득당 거래금액 주제도

# dtYearInfo = 2017
for (i in 1:length(dtYearList)) {

  dataL7 = dataL2 %>% 
    dplyr::left_join(addrData, by = c("addr" = "value")) %>% 
    dplyr::filter(
      ! is.na(lon)
      , ! is.na(lat)
      , dplyr::between(lon, 120, 130)
      , dplyr::between(lat, 30, 40)
      , dtYear == dtYearList[i]
    ) %>% 
    dplyr::group_by(emdCd) %>%
    dplyr::summarise(
      meanVal = mean(val, na.rm = TRUE)
    )
  
  mapShapeL1 = mapShape %>% 
    dplyr::inner_join(dataL7, by = c("SIG_CD" = "emdCd"))

  setTilte = stringr::str_c(dtYearList[i], "년 법정동에 따른 연소득당 거래금액 주제도")
  # saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, setTilte)
  
  tmap::tmap_mode("view")
  
  resPlot = tmap::tm_shape(mapShapeL1) +
    tmap::tm_polygons(col = "meanVal", alpha = 0.5, palette = cbMatlab, legend.hist = TRUE, style = "cont") +
    tmap::tm_text(text = "SIG_KOR_NM") +
    tmap::tm_legend(outside = TRUE) + 
    tmap::tm_layout(title = setTilte)
    # tmap_save(filename = saveImg, width = 10, height = 10, dpi = 600)

  print(resPlot)  
}



6 주택 가격 결정 요인에 대한 회귀분석

6.1 이 연구에서는 서울특별시를 사례로 2017년부터 현재 (2021년 05월)까지의 기간 동안 건축년도, 전용면적, 층, 법정동, 면적당 거래금액 등 5개 독립변수 및 종속변수인 연소득당 거래금액 (주택 가격 결정 요인)의 상대적 영향력 분석을 수행하였다.

6.2 주택 가격 결정 요인 (거래금액)를 기준으로 독립변수들 간의 관계성을 시각화

  • 상관계수의 경우 면적당 거래금액, 전용면적, 층, 건축년도 순으로 낮았고 0.01 이하의 통계적인 유의수준으로 보임

  • 서울특별시에 대한 주택 가격 결정 요인 (거래금액)에 대한 회귀분석하기 앞서 각 독립변수 및 종속 변수들 간의 관계성을 확인하기 위해서 2017-2020년 05월 기간 동안 산점도 행렬을 나타내었다.

  • 이러한 산점도 행렬은 여러 변수와 변수 간의 관계를 확인할 수 있으며 상단, 하단, 대각선으로 나타낸다. 즉 상단의 경우 상관계수 (연속량 x 연속량), 상자 그림 (연속량 x 이산량)이고 하단에서는 산점도 (연속량 × 연속량), factor 별 히스토그램 (연속량 × 이산량)이고 대각선의 경우 밀도 분포 (연속량), 막대 그래프 (이산량)으로 나타냈다.

  • 그 결과 상관계수의 경우 면적당 거래금액, 전용면적, 층, 건축년도 순으로 낮았고 0.01 이하의 통계적인 유의수준으로 보임. 밀도함수의 경우 건축년도에서는 대부분 1990 및 2000년대를 기준으로 이봉 분포를 지니고 전용 면적에서는 대형보다 중/소형의 분포가 많은 것을 알 수 있다. 더 나아가 층, 면적당 거래금액, 주택 가격 결정 요인 (연소득당 거래금액)에서는 평균 대비 좌측으로 편향됨을 보이며 이는 비이상적인 분포를 확인할 수 있었다.

# 주택 가격 결정 요인을 위한 관계성
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "주택 가격 결정 요인을 위한 관계성")

dataL2 %>%
  dplyr::select(건축년도, 전용면적, 층, val2, val) %>% 
  dplyr::rename(
    "면적당거래금액" = val2
    , "연소득당거래금액" = val
  ) %>% 
  GGally::ggpairs(.) +
  theme(text = element_text(size = 18)) # +

  # ggsave(filename = saveImg, width = 12, height = 8, dpi = 600)

6.3 주택 가격 결정 요인에 대한 상대적 영향을 분석하기 위해 베타 분석을 수행

  • 그 결과 상대적인 영향의 경우 면적당 거래금액, 전용면적, 지역구 (노원구, 강서구, 성북구 등), 층, 건축년도 순으로 낮았다 (아래 표 참조).

  • 특히 면적 관련 변수 (전용면적, 면적당 거래금액)는 0.7-0.86%를 차지하며 지역구 별로 약간의 차이를 보였다.

  • 반면에 층, 건축년도는 상대적으로 낮은 영향도를 보였다.

6.4 앞서 상대적 영향 분석을 토대로 독립변수 및 종속변수를 선정하여 단계별 소거법으로 다중선형회귀모형을 수행

  • 독립변수: 건축년도, 전용면적, 층, 지역구, 면적당 거래금액 (5개)

  • 종속변수: 연소득당 거래금액 (주택 가격 결정 요인)

  • 그 결과 수정된 결정계수는 0.89로서 0.01 이하의 유의수준을 보일 뿐만 아니라 각 회귀계수의 통계치도 유의함

dataL4 = dataL2 %>%
  dplyr::select(건축년도, 전용면적, 층, val2, d2, val)

#+++++++++++++++++++++++++++++++++++++++++++++++
# 전체 아파트
dataL5 = dataL4

# 중형 이상 아파트 (66 m2 이상)
# dataL5 = dataL4 %>% 
#   dplyr::filter(전용면적 >= 66) %>% 
#   dplyr::select(-전용면적)

# 소형 아파트 (66 m2 미만)
# dataL5 = dataL4 %>% 
#   dplyr::filter(전용면적 < 66) %>% 
#   dplyr::select(-전용면적)
#+++++++++++++++++++++++++++++++++++++++++++++++

# 선형회귀분석
lmFit = lm(val ~ ., data = dataL5)
summary(lmFit)
## 
## Call:
## lm(formula = val ~ ., data = dataL5)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -47.525  -0.821   0.273   0.873  71.294 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -4.193e+01  9.440e-01  -44.42   <2e-16 ***
## 건축년도     1.067e-02  4.735e-04   22.53   <2e-16 ***
## 전용면적     1.800e-01  1.490e-04 1208.01   <2e-16 ***
## 층           1.367e-02  7.154e-04   19.10   <2e-16 ***
## val2         1.341e-02  1.122e-05 1195.22   <2e-16 ***
## d2강동구     6.565e+00  2.769e-02  237.08   <2e-16 ***
## d2강북구     1.142e+01  3.681e-02  310.22   <2e-16 ***
## d2강서구     9.965e+00  2.762e-02  360.77   <2e-16 ***
## d2관악구     9.350e+00  3.228e-02  289.62   <2e-16 ***
## d2광진구     8.553e+00  3.620e-02  236.28   <2e-16 ***
## d2구로구     9.517e+00  2.910e-02  327.03   <2e-16 ***
## d2금천구     9.246e+00  3.878e-02  238.44   <2e-16 ***
## d2노원구     9.231e+00  2.616e-02  352.83   <2e-16 ***
## d2도봉구     9.446e+00  3.043e-02  310.38   <2e-16 ***
## d2동대문구   9.724e+00  3.097e-02  314.02   <2e-16 ***
## d2동작구     7.270e+00  3.024e-02  240.39   <2e-16 ***
## d2마포구     7.783e+00  2.982e-02  260.96   <2e-16 ***
## d2서대문구   9.766e+00  3.185e-02  306.61   <2e-16 ***
## d2서초구    -3.996e-01  2.719e-02  -14.70   <2e-16 ***
## d2성동구     7.683e+00  2.909e-02  264.10   <2e-16 ***
## d2성북구     9.748e+00  2.886e-02  337.79   <2e-16 ***
## d2송파구     4.949e+00  2.503e-02  197.73   <2e-16 ***
## d2양천구     8.085e+00  2.828e-02  285.90   <2e-16 ***
## d2영등포구   9.102e+00  2.923e-02  311.45   <2e-16 ***
## d2용산구     8.702e+00  3.468e-02  250.93   <2e-16 ***
## d2은평구     1.026e+01  3.196e-02  320.99   <2e-16 ***
## d2종로구     7.920e+00  5.005e-02  158.24   <2e-16 ***
## d2중구       6.690e+00  4.222e-02  158.47   <2e-16 ***
## d2중랑구     1.019e+01  3.266e-02  312.02   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.554 on 364888 degrees of freedom
## Multiple R-squared:  0.8947, Adjusted R-squared:  0.8947 
## F-statistic: 1.107e+05 on 28 and 364888 DF,  p-value: < 2.2e-16
# 단계별 소거법
lmFitStep = MASS::stepAIC(lmFit, direction = "both")
## Start:  AIC=684508.2
## val ~ 건축년도 + 전용면적 + 층 + val2 + d2
## 
##            Df Sum of Sq      RSS     AIC
## <none>                   2381064  684508
## - 층        1      2381  2383445  684871
## - 건축년도  1      3313  2384376  685014
## - d2       24   2007218  4388281  907567
## - val2      1   9321920 11702984 1265562
## - 전용면적  1   9522471 11903534 1271763
summary(lmFitStep)
## 
## Call:
## lm(formula = val ~ 건축년도 + 전용면적 + 층 + val2 + d2, data = dataL5)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -47.525  -0.821   0.273   0.873  71.294 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept) -4.193e+01  9.440e-01  -44.42   <2e-16 ***
## 건축년도     1.067e-02  4.735e-04   22.53   <2e-16 ***
## 전용면적     1.800e-01  1.490e-04 1208.01   <2e-16 ***
## 층           1.367e-02  7.154e-04   19.10   <2e-16 ***
## val2         1.341e-02  1.122e-05 1195.22   <2e-16 ***
## d2강동구     6.565e+00  2.769e-02  237.08   <2e-16 ***
## d2강북구     1.142e+01  3.681e-02  310.22   <2e-16 ***
## d2강서구     9.965e+00  2.762e-02  360.77   <2e-16 ***
## d2관악구     9.350e+00  3.228e-02  289.62   <2e-16 ***
## d2광진구     8.553e+00  3.620e-02  236.28   <2e-16 ***
## d2구로구     9.517e+00  2.910e-02  327.03   <2e-16 ***
## d2금천구     9.246e+00  3.878e-02  238.44   <2e-16 ***
## d2노원구     9.231e+00  2.616e-02  352.83   <2e-16 ***
## d2도봉구     9.446e+00  3.043e-02  310.38   <2e-16 ***
## d2동대문구   9.724e+00  3.097e-02  314.02   <2e-16 ***
## d2동작구     7.270e+00  3.024e-02  240.39   <2e-16 ***
## d2마포구     7.783e+00  2.982e-02  260.96   <2e-16 ***
## d2서대문구   9.766e+00  3.185e-02  306.61   <2e-16 ***
## d2서초구    -3.996e-01  2.719e-02  -14.70   <2e-16 ***
## d2성동구     7.683e+00  2.909e-02  264.10   <2e-16 ***
## d2성북구     9.748e+00  2.886e-02  337.79   <2e-16 ***
## d2송파구     4.949e+00  2.503e-02  197.73   <2e-16 ***
## d2양천구     8.085e+00  2.828e-02  285.90   <2e-16 ***
## d2영등포구   9.102e+00  2.923e-02  311.45   <2e-16 ***
## d2용산구     8.702e+00  3.468e-02  250.93   <2e-16 ***
## d2은평구     1.026e+01  3.196e-02  320.99   <2e-16 ***
## d2종로구     7.920e+00  5.005e-02  158.24   <2e-16 ***
## d2중구       6.690e+00  4.222e-02  158.47   <2e-16 ***
## d2중랑구     1.019e+01  3.266e-02  312.02   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.554 on 364888 degrees of freedom
## Multiple R-squared:  0.8947, Adjusted R-squared:  0.8947 
## F-statistic: 1.107e+05 on 28 and 364888 DF,  p-value: < 2.2e-16
# Beta 회귀계수
lmBetaFit = lm.beta::lm.beta(lmFitStep)
lmBetaFit$standardized.coefficients %>% round(2) %>% sort() %>% rev()
##        val2    전용면적    d2노원구    d2강서구    d2성북구    d2구로구 
##        0.86        0.70        0.35        0.30        0.27        0.27 
##    d2도봉구    d2은평구    d2중랑구  d2영등포구  d2동대문구    d2양천구 
##        0.25        0.24        0.23        0.23        0.23        0.22 
##  d2서대문구    d2관악구    d2강북구    d2성동구    d2마포구    d2강동구 
##        0.22        0.21        0.21        0.19        0.19        0.19 
##    d2동작구    d2용산구    d2송파구    d2금천구    d2광진구      d2중구 
##        0.17        0.16        0.16        0.16        0.15        0.10 
##    d2종로구          층    건축년도 (Intercept)    d2서초구 
##        0.09        0.01        0.01        0.00       -0.01

6.5 앞서 학습한 회귀모형을 통해 예측하여 실측과 비교한 결과

  • 향후 서울특별시에 대한 주택가격 결정 요인 (연소득당 거래금액)의 동태 효과를 보기위해 앞서 학습한 회귀모형 및 2017-2020년 05월 기간 동안 입력 자료를 기반으로 예측하여 실측과 비교함. 그 결과 상관계수 0.95로서 0.01 이하의 통계적으로 유의한 결과를 보였으나 평균제곱근오차는 2.55로서 약간의 오차를 나타냈다. 또한 연소득당 거래금액이 선형적인 관계보다 비선형 (로그, 다항함수)와 같이 급격한 상승 경향임을 알 수 있다.
# 산점도 그림
validData = data.frame(
  xAxis = predict(lmFitStep)
  , yAxis = dataL5$val
  , type = "전체 아파트"
  # , type = "중형 아파트"
  # , type = "소형 아파트"
  
)

# corVal = cor(validData$xAxis, validData$yAxis)
biasVal = Metrics::bias(validData$xAxis, validData$yAxis)
rmseVal = Metrics::rmse(validData$xAxis, validData$yAxis)

# 전체 아파트에 대한 주택가격 결정요인 (연소득당 거래금액) 예측 산점도
# saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "전체 아파트에 대한 주택가격 결정요인 예측 산점도")
# saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "중형 아파트에 대한 주택가격 결정요인 예측 산점도")
saveImg = sprintf("%s/%s_%s.png", globalVar$figPath, serviceName, "소형 아파트에 대한 주택가격 결정요인 예측 산점도")

ggscatter(
  validData, x = "xAxis", y = "yAxis", color = "black"
  , add = "reg.line", conf.int = TRUE
  , facet.by = "type"
  , add.params = list(color = "blue", fill = "lightblue")
) +
  theme_bw() +
  ggpubr::stat_regline_equation(label.x.npc = 0.0, label.y.npc = 1.0, size = 4) +
  ggpubr::stat_cor(label.x.npc = 0.0, label.y.npc = 0.9, size = 4) +
  ggpp::annotate("text_npc", npcx = 0.05, npcy = 0.8, label = sprintf("Bias = %s", round(biasVal, 2)), hjust = 0, size = 4) +
  ggpp::annotate("text_npc", npcx = 0.05, npcy = 0.7, label = sprintf("RMSE = %s", round(rmseVal, 2)), hjust = 0, size = 4) +
  # ggpp::annotate("text_npc", npcx = 0.05, npcy = 0.60, label = sprintf("Bias = %s", round(biasVal, 2)), hjust = 0, size = 4) +
  # ggpp::annotate("text_npc", npcx = 0.05, npcy = 0.55, label = sprintf("RMSE = %s", round(rmseVal, 2)), hjust = 0, size = 4) +
  labs(
    title = NULL
    , x = "예측"
    , y = "실측"
    # , subtitle = "전체 아파트에 대한 주택가격 결정요인 예측 산점도"
    # , subtitle = "중형 아파트에 대한 주택가격 결정요인 예측 산점도"
    , subtitle = "소형 아파트에 대한 주택가격 결정요인 예측 산점도"
  ) +
  theme(text = element_text(size = 16)) # +

  # ggsave(filename = saveImg, width = 6, height = 6, dpi = 600)

7 결론

  • 이러한 결과를 토대로 정부의 부동산 정책 개편되지 않을 경우 주택 시장이 점차 과열될 것으로 생각한다.

  • 그러나 최근 2021년 정부 주택 정책 (수요억제대책, 대규모 공급 대책)을 통해 주택 시장은 하락 안정세를 보일 뿐만 아니라 과대 평가된 주택시장의 거품이 빠지면서 본격적으로 주택 가격이 하락할 것으로 사료됩니다.